<?
// базовый класс с основными методами, от которого будут наследоваться остальные
class Base {
	//массив для хранения получаемых и отдаваемых значений
	protected $vars = array();

	protected static $instances=array();
	
	//базовый конструктор
	protected function __construct($vars=null) {$this->vars = $vars;}

	//здесь у нас фабрика
	static public function F($vars=null) { return new static($vars); }
	
	//здесь у нас синглетон
	static public function S($vars=null) {
		static $instance=null;
		if ($instance === null) $instance = new static($vars); 
		return $instance;
	}

	//а здесь у нас фабрика синглетонов 
	static public function G($id=null, $vars=null){ 
		if(!$id) $id = get_called_class();
		if(!isset(static::$instances[$id])) static::$instances[$id] = new static($vars); 
		return static::$instances[$id]; 
	}

	//сеттер и геттер для хранения переменных
	public function set($index, $value) { $this->vars[$index] = $value; }
	public function get($index) { return isset($this->vars[$index]) ? $this->vars[$index] : NULL; }

	static function GetInstances() {
		return self::$instances;
	}

}

//класс для получения фрагментов URL (разделенных "/")
Class Uri extends Base {
	//массив фрагментов URL
	public static $fragments;

	protected function __construct()
	{
		//преобразуем строку URL-запроса в массив и размещаем на хранение
		$this->path = $_SERVER['QUERY_STRING'];
		self::$fragments = explode('/',$this->path);
	}

	/*
	 * получение определенного фрагмента URL по ключу (порядковый номер в массиве)
	 */
	public function fragment($key)
	{

		if(array_key_exists($key,self::$fragments))
		{
			return self::$fragments[$key];
		}
		return false;
	}

}

//автозагрузчик классов на основе стандартной PHP-библиотеки SPL
Class Loader {    
	 public static function registerAutoload()
	 {
		 return spl_autoload_register(array(__CLASS__, 'includeClass'));
	 }
	 
	 public static function unregisterAutoload()
	 {
		 return spl_autoload_unregister(array(__CLASS__, 'includeClass'));
	 }
	 /*класс включается по схеме: если в коде вызвали незагруженный ранее класс, проверяем либо в папке приложения <__APP_PATH/имяКласса/имяКласса.php>,
	 либо в папке lib ядра фреймворка*/
	 public static function includeClass($class)
	 {
		$class = str_replace(__NAMESPACE__ . "\\","",$class);
		//если это контроллер, обрубаем суффикс 'Controller' в названии класса и смотрим в папку приложения
		if(strpos($class,'Controller')) {
			$class = substr($class,0,strpos($class,'Controller'));
			if(file_exists(__APP_PATH . '/' .$class.'/'.$class.'.php')) {require(__APP_PATH . '/' .$class.'/'.$class.'.php');}
			else return false;
		//иначе смотрим в папку lib ядра
		} elseif(file_exists(__VMVC_PATH . '/lib/' . strtr($class, '_\\', '//') . '.php')) require(__VMVC_PATH . '/lib/' . strtr($class, '_\\', '//') . '.php');
		elseif(file_exists(__VMVC_PATH . '/plugins/' . $class . '.php')) require(__VMVC_PATH . '/plugins/' . $class . '.php');
		elseif(is_dir(Config::S()->application->includes_dir)&&file_exists(config::S()->application->includes_dir . '/' .$class.'/'.$class.'.php'))
			require(Config::S()->application->includes_dir . '/' .$class.'/'.$class.'.php'); 
		else return false;
		
	 }
}

/*
 * класс для создания объекта-конфигурации
 * например, Config::S()->ini_section->ini_param будет содержать
 * значение параметра ini_param в секции ini_section файла конфига .ini
*/
final class Config extends Base {
	
	private static $config_file = 'config.ini';

	protected function __construct($configFile=null) {
		if($configFile) static::$config_file = realpath($configFile);
		
		if(is_file(static::$config_file )) {
			if(pathinfo(static::$config_file, PATHINFO_EXTENSION)=='ini') $params = parse_ini_file(static::$config_file, true);
			else $params = include static::$config_file;
			$this->makeObj($params);
			
		}
		define ('__APP_PATH',  dirname(static::$config_file));
	}
	
	private function makeObj($params) {
		foreach($params as $k=>$param) if(empty($this->$k)) {$this->$k = (object)$param; }
	}

	public static function setConfig($file) {
		static::$config_file = $file;
	}

}

//интерфейс контроллера
interface IController {
	//класс контроллера должен содержать по крайней мере метод indexAction - экшн по-умолчанию (index)
	public function indexAction();

}

//класс роутера
class Router extends Base {
	//переменные для хранения контроллера и экшнов
	protected $_actions=array();
	private $_controller;

	/*
	 * диспетчер действий: выполняет все заданные экшны, которые хранятся
	 * в массиве  $this->_actions
	*/
	public function dispatch($path='') {
		$this->addAction($path);

		foreach($this->_actions as $action) {
			//выполняем по-очередно все действия
			$this->Execute($action);
		}

		/*
		 * действия добавляют различные представления в глобальный
		 * синглетон вида View, вернем его просто для справки
		*/
		return View::S();
		
	}

	/*
	 * исполнитель действий
	 */
	public function Execute($action="") {

		//получаем контроллер и экшн, а после создаем отражение класса-контроллера,
		//который должен представлять интерфейс IController
		extract($this->getRoute($action));
		$rc = $this->DoReflection($controllerName);

		//имя метода-экшна в классе контроллера должно быть вида <имя>Action
		//например, updateAction()
		$getAction = $actionName .'Action';

		if($rc)
		{
			//создаем новый экземпляр контроллера
			$this->_controller = $rc->newInstance();
			// загружаем указанный ранее экшн-метод (название метода должно содержать суффикс 'Action') 
			if($rc->hasMethod( $getAction )) {
				$method = $rc->getMethod( $getAction);
				$method->invoke( $this->_controller );
				//массив имен экшенов обязательно пригодится
				$this->actionNames[] = $actionName;
			} //если метод в контроллере отсутствует, выполняем обработчик ошибок
			else {
				$this->Execute($this->GetErrorAction());
			}
			
		} //иначе формируем сообщение об ошибке для программиста, если контроллер не создан на основе интерфейса IController
		else
		{
			throw new Exception("Interface IController must be implemented");
		}
		
	}

	/*
	 * получение контроллера и экшна
	*/
	public function getRoute($action="") {
		//если путь не передан в параметре, берем его из УРЛа
		if(!$action) $action = Uri::S()->path;

		//путь роутинга вида <имя_контроллера>/<имя_экшна>, надо его разбить на состовляющие
		$controller_action = explode('/',$action);

		//получаем предварительно подчищенные имена контроллера и экшна
		$controllerName = $controller_action[0]?(strpos($controller_action[0],'&')?substr($controller_action[0],0,strpos($controller_action[0],'&')):$controller_action[0]):'index';
		$actionName = $controller_action[1]&&strpos($controller_action[1],'&')===false?$controller_action[1]:'index';

		//возвращаем их массивом
		return compact('controllerName','actionName');
	}

	//вернет отражение класса контроллера, если контроллер предствляет интерфейс IController
	private function DoReflection($con) {
		$rc = new \ReflectionClass( __NAMESPACE__ . "\\" . $con.'Controller' );
		// если контроллер отражает интерфейс IController
		return $rc->implementsInterface( __NAMESPACE__ . "\\" . 'IController' )?$rc:false;
	}
	
	//вернет название обработчика ошибок в формате, понятном роутеру
	private function GetErrorAction() {
		$config = Config::S();
		return isset($config->application->error_action)?$config->application->error_action:'index/index';
	}

	/**
	*
	* Добавляем экшн
	*
	*/
	public function addAction($action="")
	{
		if(!in_array($action,$this->_actions)) $this->_actions[]=$action;
	}

	public function getActions() {
		return $this->actionNames;
	}

	//вернет экземплря контроллера
	public function GetController() {
		if($this->_controller) return $this->_controller;
	}


}

//класс для загрузки и отображения шаблонов и представлений
class View extends Base
{

	//Название папки, где хранятся шаблоны 
	private $tpl_dir = null;
	//файл основного шаблона
	private $layout = null;

	//коллекция представлений
	private $_views=array();
	//коллекция представлений, которые не нужно выводить
	private $_rmviews=array();
	//расширение файлов представлений и шаблонов
	public $ext='.php';

	protected function __construct() {
		$config = Config::S();
		//задаем директорию шаблонов и имя файла главного шаблона-раскладки
		if(isset($config->template)) {
			$this->setTemplateDir(__APP_PATH. (isset($config->template->tpl_dir)?'/'.$config->template->tpl_dir:''));
			$this->setLayout((isset($config->template->layout)?$config->template->layout:'layout'));
		}
		else {
			$this->setTemplateDir(__APP_PATH);
			$this->setLayout('layout');
		}
	}

	//выводим основной шаблон, предварительно отразив в нем все представления
	public function output()
	{
		foreach($this->_views as $view) $this->view.=$view;
		$this->addView($this->layout,'/');
		echo end($this->_views);
	}

	//формируем представление, помещаем его в массив $this->_views
	function addView( $viewfile , $viewdir='/views/' )
	{
		//если представление значится как скрытое, ничего не делаем
		if(in_array($viewfile,$this->_rmviews)) return;
		else {
			$this->_views[] = $this->fetch($viewfile,$viewdir); 
		}
	}
	//помещает представление в скрытые
	function removeView($viewfile) {
		$this->_rmviews[]=$viewfile;
	}
	//получим содержимое представления
	function fetch($viewfile, $viewdir='/views/') {
		/*** извлекаем все переменные ***/
		extract( (array)$this->vars );
		$viewfile = $this->tpl_dir . $viewdir . $viewfile .$this->ext;
		if (file_exists($viewfile))
		{
			ob_start();
			include($viewfile);
			$output = ob_get_contents();
			ob_end_clean();
			return $output;
		}
		else
		{
			throw new \Exception("The view file '$viewfile' does not exist", 200);
		}
	}

	//метод задает директорию шаблонов
	public function setTemplateDir($dir)
	{
		$tpl_dir = realpath($dir);
		if (is_dir($tpl_dir))
		{
			$this->tpl_dir = $tpl_dir;
		} //если директория не создана, сообщаем об этом программисту
		else
		{
			throw new \Exception("The template directory '$dir' does not exist", 200);
		}
	}

	//вернет директорию шаблонов
	public function getTemplateDir() {
		return $this->tpl_dir;
	}

	//задает файл основного шаблона
	public function setLayout($file) {
		$this->layout=$file;
	}

	/*
	 * сеттер и геттер для хранения переменных
	 * обычно, это те переменные, которые нужно вывести в представлении
	 */
	public function __set($index, $value) { $this->vars[$index] = $value;}
	public function __get($index) { return isset($this->vars[$index]) ? $this->vars[$index] : NULL; }
	
}


/*
 * класс реализующий систему хуков в простейшем виде,
 * возможно, позже будет переделан в шаблон проектирования listener;
 * удобно применять в шаблонах для пакетной замены одних данных другими (к примеру, превращения относительных ссылок в абсолютные)
 */
Class Hooks extends Base {

	//все файлы хуков
	private $_hooksfiles=array();
	
	protected function __construct() {
		//если существует папка с файлами хуков, инклудим ее
		if(is_dir(__APP_PATH.'/'.Config::S()->application->includes_dir.'/hooks/')) $this->_hooksfiles = vmvc::autoinc(__APP_PATH.'/'.Config::S()->application->includes_dir.'/hooks/');
	}

	//выполняем хуки
	function __call($method,$arguments) {
		foreach ($this->_hooksfiles as $hookname) {
			//имя класса хука должно иметь суффикс Hook 
			$hook = __NAMESPACE__ . "$hookname".'Hook';

			//проверяем, наличествует ли метод в классе хука, например, beforeRender()
			if(method_exists($hook::S(),$method)) $hook::S()->$method($arguments);

		}
	}
		
}

//класс-реестр
final class R extends Base {

	public function has($key) {
    	return isset($this->vars[$key]);
  	}

}

//главный класс фреймворка, задает последовательность работы
class vmvc extends Base {
	
	//метод для запуска приложения
	public static function Go($configFile) {

		//получим абсолютный путь к папке фреймворка и определим соотв. глобальную константу
		define ('__VMVC_PATH', dirname(__FILE__));

		switch(Config::S($configFile)->application->errr) { 
		 case 0: error_reporting(0); break; 
		 case 1: error_reporting(E_ERROR | E_WARNING | E_PARSE); break; 
		 case 2: error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE); break; 
		 case 3: error_reporting(E_ALL ^ (E_NOTICE | E_WARNING)); break; 
		 case 4: error_reporting(E_ALL ^ E_NOTICE); break; 
		 case 5: error_reporting(E_ALL); break; 
		 default: 
			 error_reporting(E_ALL); 
		}
		
		//запускаем автозагрузчик классов
		Loader::registerAutoload();

		//получим абсолютный путь к папке сайта и определим соотв. глобальную константу
		define ('__SITE_PATH', dirname(Request::S()->server["SCRIPT_FILENAME"]));

		//получим URL-адрес сайта
		define('__SITE_LIVE',(isset(Request::S()->server["REQUEST_SCHEME"])?Request::S()->server["REQUEST_SCHEME"]:'http').'://'.Request::S()->server['HTTP_HOST'].rtrim(Request::S()->server['PHP_SELF'],'index.php'));

		//выполняем перед роутингом все хуки
		Hooks::S()->beforeRoute();

		//запускаем роутинг
		$response = Router::S()->dispatch();

		//выполняем перед рендерингом все хуки
		Hooks::S()->beforeRender();

		//выводим шаблон
		$response->output();

		//выполяем все хуки после вывода шаблона
		Hooks::S()->afterDisplayTemplate();
		
	}

	//простой рекурсивный инклудер дополнительных библиотек 
	public static function autoinc($libpath) {
		$incfiles = array();
		$dhdl = opendir($libpath);
		
		while($file = readdir($dhdl)) {
			if($file!='.'&&$file!='..'){
				if (is_file("$libpath/$file")) {
					include_once "$libpath/$file";
					$file_info = pathinfo($file);
					$file_name =  basename($file,'.'.$file_info['extension']);			
					$incfiles[]=$file_name;
					}
				else $incfiles[]=self::autolib("$libpath/$file");
			}
		}
		return $incfiles;
	}

}


?>
